package biggestloser.util;

import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.logging.Logger;

import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.FetchOptions;
import com.google.appengine.api.datastore.PreparedQuery;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.Query.FilterOperator;
import com.google.appengine.api.datastore.Query.FilterPredicate;

/**
 * List of static utility methods for accessing the Data Store.
 * @author Mike
 */
public class DatastoreUtil
{
	private static final Logger log = Logger.getLogger(DatastoreUtil.class.getName());
	
	public static List<Entity> getUsers()
	{
		List<Entity> users = new ArrayList<Entity>();
		
		DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
        Query q = new Query("User");
	    PreparedQuery pq = datastore.prepare(q);
	    
	    for (Entity user : pq.asIterable())
		{
	    	users.add(user);
		}
		
		return users;
	}
	
	public static List<Entity> getEnabledUsers()
	{
		List<Entity> users = new ArrayList<Entity>();
		
		DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
        Query q = new Query("User");
        
		q.setFilter(
				new FilterPredicate("status", FilterOperator.NOT_EQUAL, "disabled")
		);
        
	    PreparedQuery pq = datastore.prepare(q);
	    
	    for (Entity user : pq.asIterable())
		{
	    	users.add(user);
		}
	    
		return users;
	}
	
	/**
	 * Gets the user Entity from the data store who matches the given e-mail
	 * address. 
	 * @param email E-mail address of user.
	 * @return Entity object for the user, or null if not found.
	 */
	public static Entity getUserByEmail(String email)
	{
		DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
		
		// Make sure this is a valid user
		Query userQ = new Query("User");
		userQ.setFilter(
				new FilterPredicate("email", FilterOperator.EQUAL, email)
		);

		PreparedQuery userPq = datastore.prepare(userQ);
		if(userPq.countEntities(FetchOptions.Builder.withDefaults()) == 0)
		{
			return null;
		}
		else				
		{
			return userPq.asSingleEntity();
		}
	}
	
	/**
	 * Generates the data need for the table chart on the Main servlet.
	 * @return String of table data.
	 */
	public static String getTableChartData()
	{
		// The format for table charts is as follows:
		// [
        //  ['Mike',  {v: 0.0, f: '0.0%'}, {v: -1.0, f: '-1.0%'}, {v: -1.9, f: '-1.9%'}, {v: -2.5, f: '-2.5%'}, ],
        //  ['John',  {v: 0.0, f: '0.0%'}, {v: -1.0, f: '-1.0%'}, {v: -1.3, f: '-1.3%'}, {v: -2.9, f: '-2.9%'}, ]
		// ]
		//
		// To achieve this, we'll use a List of StringBuffers, where each entry is a row of data
		List<StringBuffer> data = new ArrayList<StringBuffer>();
		
		// Used to format percents
		DecimalFormat weightFormat = new DecimalFormat("#0.00");
		
		List<Entity> users = DatastoreUtil.getEnabledUsers();
		long week = DatastoreUtil.getCurrentWeek();
		
		for(int i = 0; i < users.size(); i++)
		{
			Entity user = users.get(i);
			
			StringBuffer sb = new StringBuffer();
			sb.append("[");
			
			// Remember some user's have two words in their
			// last name, so we use the first " " character to find the
			// complete last name.
			String fullName = user.getProperty("name").toString();
			
			sb.append("'").append(fullName).append("', ");
			
			// Week 1 has no change data, it gets a non-numeric value
			sb.append("{v: 0.0, f: '0.0%'} , ");
			
			// Only weeks 2 and beyond have change data
			for(long j = 2; j <= 12; j++)
			{
				// Only print data for weeks we have completed; future weeks get a '--'
				//
				// Note: The chart.jsp file formats all future weeks as string instead
				// of number; make sure not to send back '--' when number is expected
				// and vice versa.
				if(j <= week)
				{
					// We may still be waiting on the user to send in results, so we
					// default to 0.0 with a special markup to indicate as much.
					double change = 0.0;
					String changeStr = "<font color=\"#c1c1c1\">0.00%</font>";
					
					if(user.getProperty("week" + j + "change") != null)
					{
						change    = Double.parseDouble(user.getProperty("week" + j + "change").toString());
						
						if(change <= 0.0)
						{
							changeStr = "<font color=\"green\">" + weightFormat.format(change) + "%</font>";
						}
						else
						{
							changeStr = "<font color=\"red\">" + weightFormat.format(change) + "%</font>";
						}
					}
					else
					{
						// No data submitted this week; set a cell format option to highlight this
						//formatOptions.add(i + ", " + j + ", " + change + ", '" + changeStr + "', {style: 'font-style:bold'}");
					}
					
					sb.append("{v: " + change + ", f: '" + changeStr + "'}");
				}
				else
				{
					sb.append("' '");
				}
				
				// See if we need a comma or not
				if(j < 12)
				{
					sb.append(" , ");
				}
			}
			
			sb.append("]");
			
			// If this is not the last week for this user, add a comma
			if(i < users.size() - 1)
			{
				sb.append(",");
			}
			
			// Add new line
			sb.append("\n");
			
			
			data.add(sb);
		}
		
		StringBuffer sb = new StringBuffer();
		
		for(StringBuffer row : data)
		{
			sb.append(row.toString());
		}
		
		return "[\n" + sb.toString() + "\n]";
	}
	
	/**
	 * Generates data for the line chart used in the Main servlet.
	 * @param formatOptions This will be populated with a list of formatting
	 * options to highlight certain conditions in the table, for example when
	 * a user's weekly data is missing.
	 * @return String of array data.
	 */
	public static String getLineChartData()
	{
		// The format of the data is a little obscure for line charts. It's a
		// two dimensional array where each column is a series. The first column
		// is the horizontal access. See the link below for more details:
		//
		// https://google-developers.appspot.com/chart/interactive/docs/gallery/linechart
		//
		// Here's a sample array:
		//
		// [ 
		//   ['Week', 'Pratt', 'Doe'],
		//   ['1',     0,       0   ],
		//   ['2',     -1.5,    -1.2],
		//   ['3',     -2.2,    -2.1]
		// ]
		//
		// Because of this odd way of doing rows and columns, to avoid looping
		// over the users multiple times, we build an array of Strings, where
		// each String is a week of data.  We only generate data up to, and
		// including, the current week of competition.
		
		// This is the array of Strings (using StringBuffer for performance)
		List<StringBuffer> arrayData = new ArrayList<StringBuffer>();
		
		// Get all users
		List<Entity> users = DatastoreUtil.getEnabledUsers();
		
		// Get the current week of competition
		long week = DatastoreUtil.getCurrentWeek();
		
		// Used to format % lost
		DecimalFormat weightFormat = new DecimalFormat("#0.00");
		
		// Adding an extra iteration to handle header row
		for(long i = 0; i <= week; i++)
		{
			StringBuffer sb = new StringBuffer();
			sb.append("[");

			// The first column is either the word "Week" or the actual
			// week number
			if(i == 0)
			{
				sb.append("'Week' , ");
			}
			else
			{
				sb.append("'" + i + "' , ");
			}
			
			for(int j = 0; j < users.size(); j++)
			{
				Entity user = users.get(j);
				
				if(i == 0)
				{
					// Header row, add the user's last name to keep the legend
					// compact. Remember some user's have two words in their
					// last name, so we use the first " " character to find the
					// complete last name.
					String fullName = user.getProperty("name").toString();
					String lastName = fullName.substring(fullName.indexOf(' '));
					
					sb.append("'").append(fullName.charAt(0)).append(". ").append(lastName).append("'");
					
				}
				else if(i == 1)
				{
					// Week 1 is always 0.0
					sb.append("0.0");
				}
				else
				{
					// % change from week 1
					double week1 = Double.parseDouble(user.getProperty("week1").toString());
					double currWeek = Double.parseDouble(user.getProperty("week" + i).toString());
					
					// User may not have submitted week's weigh in; if not then loop backwards until
					// we eight find the last submitted weight or just return 0.0
					double change = 0.0;
					if(currWeek > 0.0)
					{
						change  = Double.parseDouble(user.getProperty("week" + i + "change").toString());
					}
					else
					{
						for(long k = i - 1; k >= 2; k--)
						{
							if(user.getProperty("week" + (k) + "change") != null)
							{
								change = Double.parseDouble(user.getProperty("week" + (k) + "change").toString());
								break;
							}
						}
					}
					
		    		String changeStr = weightFormat.format(change);
		    		sb.append(changeStr);
				}
				
				// Either add a ',' or ']'
				if(j != users.size() - 1)
				{
					sb.append(" , ");
				}
			}
			
			// We're now done with a row of data; it at least needs a ']'
			// and possibly a ',' if it's not the last row
			sb.append("]");
			if(i < week)
			{
				sb.append(",\n");
			}
			
			// Finish the row and add to the array
			arrayData.add(sb);
		}
		
		String arrayStr = "";
		
		for(StringBuffer sb : arrayData)
		{
			arrayStr += sb.toString();
		}
		
		return "[\n" + arrayStr + "]\n";
	}
	
	public static long getCurrentWeek()
	{
		// TODO: Make this properties driven
        GregorianCalendar today = new GregorianCalendar();
		GregorianCalendar start = new GregorianCalendar(2013, 0, 10);
		
		return (today.getTimeInMillis() - start.getTimeInMillis()) / (86400 * 1000 * 7) + 1 ;
	}
	
	public static long getDaysRemaining()
	{
		// TODO: Make this properties driven
		long totalDays = 12 * 7;
		
        GregorianCalendar today = new GregorianCalendar();
		GregorianCalendar start = new GregorianCalendar(2013, 0, 10);
		
		return totalDays - (today.getTimeInMillis() - start.getTimeInMillis()) / (86400 * 1000);
	}
	
	/**
	 * Calculates total weight loss by all users. Since some users don't respond
	 * every week and not everyone responds at the same time, we loop backwards
	 * from the last week for each user until we find a valid changePct attribute.
	 * @return Total weight loss for all users, in pounds.
	 */
	public static String getTotalWeightLoss()
	{
		List<Entity> users = DatastoreUtil.getUsers();
		
		double total = 0.0;
		
		// Used to format % lost
		DecimalFormat weightFormat = new DecimalFormat("#000.0");
		
		for(Entity user : users)
		{	
			// Make sure they have a week1 weight
			if(user.getProperty("week1") == null)
			{
				continue;
			}
			
			double week1 = Double.parseDouble(user.getProperty("week1").toString());
			
			for(int i = 12; i >= 2; i--)
			{
				// Keep going until we find a weight for a week
				if(user.getProperty("week" + i) == null)
				{
					continue;
				}
				
				double currWeek = Double.parseDouble(user.getProperty("week" + i).toString());
				
				if(currWeek == 0.0)
				{
					continue;
				}
				
				// We found the most recent weight; add the change since week 1
				// to the running total and then break out of of this loop
				double change = week1 - currWeek;
				
				total += change;
				
				break;
			}
		}
		
		return weightFormat.format(total);
	}
}
